﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;


/***********************************************************************************************************************/
// package head
//					id					  length	seg count seg id  check			message
// |								|				|		|		|		|   ...    ...   ...
// 0								8				12		14		16		18
/***********************************************************************************************************************/

namespace Sttplay.Net.UDP
{
    public class RUDPEx : RUDP
    {
        private class Package
        {
            public uint length;
            public uint crtLen;
            public uint updatets;
            public byte[] data;
        }
        private const int PACKAGE_HEAD_SIZE = 18;
        private ushort pkgID;
        private const int segment = RCTP.BUFF_SIZE - PACKAGE_HEAD_SIZE;
        private Dictionary<ulong, Package> packages = new Dictionary<ulong, Package>();
        private uint lastCheckTimeoutTs = 0;

        public RUDPEx() : base()
        {

        }

        public override bool Initialize(IPAddress ip, int port)
        {
            if (segment > RCTP.BUFF_SIZE - PACKAGE_HEAD_SIZE)
                throw new Exception(string.Format("segment len ({0}) > valid len ({1})", segment, RCTP.BUFF_SIZE - PACKAGE_HEAD_SIZE));
            pkgID = (ushort)(RCTP.RandomSingle() * 10000);
            return base.Initialize(ip, port);
        }

        public override void Update(int timeout = 0)
        {
            base.Update(timeout);
            if(GetTimestamp() - lastCheckTimeoutTs >= 1000)
            {
                lastCheckTimeoutTs = GetTimestamp();
                List<ulong> removes = new List<ulong>();
                foreach (var item in packages)
                {
                    if (GetTimestamp() - item.Value.updatets > 10000)
                        removes.Add(item.Key);
                }
                for (int i = 0; i < removes.Count; i++)
                    packages.Remove(removes[i]);
            }
        }
        public override void SendTo(byte[] buffer, int index, int len, EndPoint remote, bool reliable)
        {
            if (reliable && joinAddress == null)
            {
                ulong pid = AssemblyPackageID(pkgID++, (ushort)(RCTP.RandomSingle() * 10000), RCTP.GetTimestamp());
                int offset = 0;
                uint msgLen = (uint)len;
                ushort segID = 0;
                ushort segCount = (ushort)(msgLen / segment + (msgLen % segment == 0 ? 0 : 1));
                while (offset < msgLen)
                {
                    int realLen = (int)Math.Min(msgLen - offset, segment);
                    byte[] sendbuf = AssemblyPackageBuffer(pid, msgLen, segCount, segID, buffer, index + offset, realLen);
                    base.SendTo(sendbuf, 0, sendbuf.Length, remote, true);
                    offset += realLen;
                    segID++;
                }
            }
            else
            {
                base.SendTo(buffer, index, len, remote, false);
            }
        }

        protected override void OnRecvEvent(byte[] buffer, bool reliable, object param)
        {
            if (!reliable)
            {
                base.OnRecvEvent(buffer, reliable, param);
                return;
            }
            if (!VerifyBuffer(buffer, 0, buffer.Length))
            {
                base.OnRecvEvent(buffer, reliable, param);
                return;
            }
            try
            {
                ulong pid = BitConverter.ToUInt64(buffer, 0);
                uint length = BitConverter.ToUInt32(buffer, 8);
                ushort segCount = BitConverter.ToUInt16(buffer, 12);
                ushort segID = BitConverter.ToUInt16(buffer, 14);
                if (segCount == 1)
                {
                    byte[] temp = new byte[buffer.Length - PACKAGE_HEAD_SIZE];
                    Buffer.BlockCopy(buffer, PACKAGE_HEAD_SIZE, temp, 0, buffer.Length - PACKAGE_HEAD_SIZE);
                    base.OnRecvEvent(temp, reliable, param);
                }
                else
                {
                    if (packages.ContainsKey(pid))
                    {
                        Package pkg = packages[pid];
                        Buffer.BlockCopy(buffer, PACKAGE_HEAD_SIZE, pkg.data, (int)(segment * segID), buffer.Length - PACKAGE_HEAD_SIZE);
                        pkg.crtLen += (uint)(buffer.Length - PACKAGE_HEAD_SIZE);
                        pkg.updatets = GetTimestamp();
                        if (pkg.crtLen >= pkg.length)
                        {
                            base.OnRecvEvent(pkg.data, reliable, param);
                            packages.Remove(pid);
                        }
                    }
                    else
                    {
                        Package pkg = new Package();
                        pkg.crtLen = (uint)(buffer.Length - PACKAGE_HEAD_SIZE);
                        pkg.length = length;
                        pkg.data = new byte[length];
                        pkg.updatets = GetTimestamp();
                        Buffer.BlockCopy(buffer, PACKAGE_HEAD_SIZE, pkg.data, (int)(segment * segID), buffer.Length - PACKAGE_HEAD_SIZE);
                        packages.Add(pid, pkg);
                    }
                }
            }
            catch (Exception ex)
            {
                OnErrorLogEvent(ex.ToString());
            }
        }
        private byte[] AssemblyPackageBuffer(ulong pid, uint length, ushort segCount, ushort segID, byte[] data, int index, int len)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                var pidbs = BitConverter.GetBytes(pid);
                var lenbs = BitConverter.GetBytes(length);
                var segcbs = BitConverter.GetBytes(segCount);
                var segibs = BitConverter.GetBytes(segID);
                ms.Write(pidbs, 0, sizeof(ulong));
                ms.Write(lenbs, 0, sizeof(uint));
                ms.Write(segcbs, 0, sizeof(ushort));
                ms.Write(segibs, 0, sizeof(ushort));
                ushort sum = 0;
                for (int i = 0; i < 8; i++)
                    sum += pidbs[i];
                for (int i = 0; i < 4; i++)
                    sum += lenbs[i];
                for (int i = 0; i < 2; i++)
                    sum += segcbs[i];
                for (int i = 0; i < 2; i++)
                    sum += segibs[i];
                ms.Write(BitConverter.GetBytes(sum), 0, sizeof(ushort));
                ms.Write(data, index, len);
                ms.Position = 0;
                byte[] buffer = new byte[ms.Length];
                ms.Read(buffer, 0, buffer.Length);
                return buffer;
            }
        }

        private bool VerifyBuffer(byte[] buffer, int index, int length)
        {
            if (length < PACKAGE_HEAD_SIZE)
                return false;
            int sum = 0;
            for (int i = 0; i < 16; i++)
                sum += buffer[i + index];
            if (sum != BitConverter.ToUInt16(buffer, 16 + index))
                return false;
            return true;
        }
        private ulong AssemblyPackageID(ushort lid, ushort r, uint ts)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                ms.Write(BitConverter.GetBytes(lid), 0, sizeof(ushort));
                ms.Write(BitConverter.GetBytes(r), 0, sizeof(ushort));
                ms.Write(BitConverter.GetBytes(ts), 0, sizeof(uint));
                ms.Position = 0;
                byte[] buffer = new byte[ms.Length];
                ms.Read(buffer, 0, buffer.Length);
                return BitConverter.ToUInt64(buffer, 0);
            }
        }
        ~RUDPEx()
        {
            Terminate();
        }
    }
}